L.interpolatePosition = function(p1, p2, duration, t) {
  var k = t/duration;
  k = (k > 0) ? k : 0;
  k = (k > 1) ? 1 : k;
  return L.latLng(p1.lat + k * (p2.lat - p1.lat),
      p1.lng + k * (p2.lng - p1.lng));
};

L.Marker.MovingMarker = L.Marker.extend({

  //state constants
  statics: {
      notStartedState: 0,
      endedState: 1,
      pausedState: 2,
      runState: 3
  },

  options: {
      autostart: false,
      loop: false,
  },

  initialize: function (latlngs, durations, options) {
      L.Marker.prototype.initialize.call(this, latlngs[0], options);

      this._latlngs = latlngs.map(function(e, index) {
          return L.latLng(e);
      });

      if (durations instanceof Array) {
          this._durations = durations;
      } else {
          this._durations = this._createDurations(this._latlngs, durations);
      }

      this._currentDuration = 0;
      this._currentIndex = 0;

      this._state = L.Marker.MovingMarker.notStartedState;
      this._startTime = 0;
      this._startTimeStamp = 0;  // timestamp given by requestAnimFrame
      this._pauseStartTime = 0;
      this._animId = 0;
      this._animRequested = false;
      this._currentLine = [];
      this._stations = {};
  },

  isRunning: function() {
      return this._state === L.Marker.MovingMarker.runState;
  },

  isEnded: function() {
      return this._state === L.Marker.MovingMarker.endedState;
  },

  isStarted: function() {
      return this._state !== L.Marker.MovingMarker.notStartedState;
  },

  isPaused: function() {
      return this._state === L.Marker.MovingMarker.pausedState;
  },

  start: function() {
      if (this.isRunning()) {
          return;
      }

      if (this.isPaused()) {
          this.resume();
      } else {
          this._loadLine(0);
          this._startAnimation();
          this.fire('start');
      }
  },

  resume: function() {
      if (! this.isPaused()) {
          return;
      }
      // update the current line
      this._currentLine[0] = this.getLatLng();
      this._currentDuration -= (this._pauseStartTime - this._startTime);
      this._startAnimation();
  },

  pause: function() {
      if (! this.isRunning()) {
          return;
      }

      this._pauseStartTime = Date.now();
      this._state = L.Marker.MovingMarker.pausedState;
      this._stopAnimation();
      this._updatePosition();
  },

  stop: function(elapsedTime) {
      if (this.isEnded()) {
          return;
      }

      this._stopAnimation();

      if (typeof(elapsedTime) === 'undefined') {
          // user call
          elapsedTime = 0;
          this._updatePosition();
      }

      this._state = L.Marker.MovingMarker.endedState;
      this.fire('end', {elapsedTime: elapsedTime});
  },

  addLatLng: function(latlng, duration) {
      this._latlngs.push(L.latLng(latlng));
      this._durations.push(duration);
  },

  moveTo: function(latlng, duration) {
      this._stopAnimation();
      this._latlngs = [this.getLatLng(), L.latLng(latlng)];
      this._durations = [duration];
      this._state = L.Marker.MovingMarker.notStartedState;
      this.start();
      this.options.loop = false;
  },

  addStation: function(pointIndex, duration) {
      if (pointIndex > this._latlngs.length - 2 || pointIndex < 1) {
          return;
      }
      this._stations[pointIndex] = duration;
  },

  onAdd: function (map) {
      L.Marker.prototype.onAdd.call(this, map);

      if (this.options.autostart && (! this.isStarted())) {
          this.start();
          return;
      }

      if (this.isRunning()) {
          this._resumeAnimation();
      }
  },

  onRemove: function(map) {
      L.Marker.prototype.onRemove.call(this, map);
      this._stopAnimation();
  },

  _createDurations: function (latlngs, duration) {
      var lastIndex = latlngs.length - 1;
      var distances = [];
      var totalDistance = 0;
      var distance = 0;

      // compute array of distances between points
      for (var i = 0; i < lastIndex; i++) {
          distance = latlngs[i + 1].distanceTo(latlngs[i]);
          distances.push(distance);
          totalDistance += distance;
      }

      var ratioDuration = duration / totalDistance;

      var durations = [];
      for (i = 0; i < distances.length; i++) {
          durations.push(distances[i] * ratioDuration);
      }

      return durations;
  },

  _startAnimation: function() {
      this._state = L.Marker.MovingMarker.runState;
      this._animId = L.Util.requestAnimFrame(function(timestamp) {
          this._startTime = Date.now();
          this._startTimeStamp = timestamp;
          this._animate(timestamp);
      }, this, true);
      this._animRequested = true;
  },

  _resumeAnimation: function() {
      if (! this._animRequested) {
          this._animRequested = true;
          this._animId = L.Util.requestAnimFrame(function(timestamp) {
              this._animate(timestamp);
          }, this, true);
      }
  },

  _stopAnimation: function() {
      if (this._animRequested) {
          L.Util.cancelAnimFrame(this._animId);
          this._animRequested = false;
      }
  },

  _updatePosition: function() {
      var elapsedTime = Date.now() - this._startTime;
      this._animate(this._startTimeStamp + elapsedTime, true);
  },

  _loadLine: function(index) {
      this._currentIndex = index;
      this._currentDuration = this._durations[index];
      this._currentLine = this._latlngs.slice(index, index + 2);
  },

  /**
   * Load the line where the marker is
   * @param  {Number} timestamp
   * @return {Number} elapsed time on the current line or null if
   * we reached the end or marker is at a station
   */
  _updateLine: function(timestamp) {
      // time elapsed since the last latlng
      var elapsedTime = timestamp - this._startTimeStamp;

      // not enough time to update the line
      if (elapsedTime <= this._currentDuration) {
          return elapsedTime;
      }

      var lineIndex = this._currentIndex;
      var lineDuration = this._currentDuration;
      var stationDuration;

      while (elapsedTime > lineDuration) {
          // substract time of the current line
          elapsedTime -= lineDuration;
          stationDuration = this._stations[lineIndex + 1];

          // test if there is a station at the end of the line
          if (stationDuration !== undefined) {
              if (elapsedTime < stationDuration) {
                  this.setLatLng(this._latlngs[lineIndex + 1]);
                  return null;
              }
              elapsedTime -= stationDuration;
          }

          lineIndex++;

          // test if we have reached the end of the polyline
          if (lineIndex >= this._latlngs.length - 1) {

              if (this.options.loop) {
                  lineIndex = 0;
                  this.fire('loop', {elapsedTime: elapsedTime});
              } else {
                  // place the marker at the end, else it would be at
                  // the last position
                  this.setLatLng(this._latlngs[this._latlngs.length - 1]);
                  this.stop(elapsedTime);
                  return null;
              }
          }
          lineDuration = this._durations[lineIndex];
      }

      this._loadLine(lineIndex);
      this._startTimeStamp = timestamp - elapsedTime;
      this._startTime = Date.now() - elapsedTime;
      return elapsedTime;
  },

  _animate: function(timestamp, noRequestAnim) {
      this._animRequested = false;

      // find the next line and compute the new elapsedTime
      var elapsedTime = this._updateLine(timestamp);

      if (this.isEnded()) {
          // no need to animate
          return;
      }

      if (elapsedTime != null) {
           // compute the position
          var p = L.interpolatePosition(this._currentLine[0],
              this._currentLine[1],
              this._currentDuration,
              elapsedTime);
          this.setLatLng(p);
      }

      if (! noRequestAnim) {
          this._animId = L.Util.requestAnimFrame(this._animate, this, false);
          this._animRequested = true;
      }
  }
});

L.Marker.movingMarker = function (latlngs, duration, options) {
  return new L.Marker.MovingMarker(latlngs, duration, options);
};